-
Notifications
You must be signed in to change notification settings - Fork 439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add an API for editing a group membership's role #9114
Conversation
def asdict(self): | ||
return { | ||
"authority": self.membership.group.authority, | ||
"userid": self.membership.user.userid, | ||
"username": self.membership.user.username, | ||
"display_name": self.membership.user.display_name, | ||
"roles": self.membership.roles, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is compatible with the user objects currently returned by the get-group-members API just with roles
added. (It was previously thought of as a list of users, but now we're thinking of it as a list of memberships.)
return [ | ||
GroupMembershipJSONPresenter(membership).asdict() | ||
for membership in context.group.memberships | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The get-group-members API now uses the new GroupMembershipJSONPresenter
instead of UserJSONPresenter
. It returns the same dicts as UserJSONPresenter
does but with roles
added. This means that this PR also adds roles
to the get-group-members API responses.
@@ -28,7 +36,6 @@ def list_members(context: GroupContext, _request): | |||
permission=Permission.Group.MEMBER_REMOVE, | |||
) | |||
def remove_member(context: GroupMembershipContext, request): | |||
"""Remove a member from a group.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing these pointless docstrings.
if not request.has_permission( | ||
Permission.Group.MEMBER_EDIT, | ||
EditGroupMembershipContext( | ||
context.group, context.user, context.membership, new_roles | ||
), | ||
): | ||
raise HTTPNotFound() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no permission
in the @api_config()
on this view. The reason is that whether or not you have permission to change a role depends on the new role that you want to change it to (only owners can change someone's role to owner or admin, admins can change someone's role to moderator or member, etc).
The new role comes from the request JSON body and isn't available until that JSON body has been parsed and validated, and that happens in the view. So we can't use Pyramid's permission
thing where it calls the security policy before calling the view.
Instead we let Pyramid call the view regardless, and then the view itself constructs a context object and calls request.has_permission()
.
We do need the permissions logic to be implemented in the centralized permissions system because in future another view is going to have to check MEMBER_EDIT
permissions as well, so we couldn't just code the logic directly in this view. Also permissions logic just belongs in the permissions system.
h/views/api/group_members.py
Outdated
): | ||
raise HTTPNotFound() | ||
|
||
context.membership.roles = new_roles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no need for a service to carry out the business logic, it's just an assignment.
old_roles, | ||
) | ||
|
||
return GroupMembershipJSONPresenter(context.membership).asdict() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The edit_member()
and list_members()
views both use GroupMembershipJSONPresenter
so they both return groups in the same format.
If we ever add a get_member()
view it can use the same presenter as well.
add_member()
could also use it as well but that might be a breaking change (currently it returns an HTTP 204 with no body).
0c02bd7
to
d0791b7
Compare
assert res.json == [ | ||
{ | ||
"authority": membership.group.authority, | ||
"userid": membership.user.userid, | ||
"username": membership.user.username, | ||
"display_name": membership.user.display_name, | ||
"roles": membership.roles, | ||
} | ||
for membership in group.memberships | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made this test assertion more specific so I can see exactly what changes I'm making to the get-group-members API (adding the roles
)
assert res.json == [ | ||
{ | ||
"authority": membership.group.authority, | ||
"userid": membership.user.userid, | ||
"username": membership.user.username, | ||
"display_name": membership.user.display_name, | ||
"roles": membership.roles, | ||
} | ||
for membership in group.memberships | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
79efb82
to
702c89e
Compare
a0a645d
to
078fb8c
Compare
702c89e
to
b30f3cd
Compare
old_roles = context.membership.roles | ||
new_roles = context.new_roles | ||
|
||
def get_authenticated_users_roles(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and the equivalent get_authenticated_users_membership
could be eventually refactored and be a helper method on identity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's a good idea. I'll add a card for this to Misha's onboarding project
h/security/predicates.py
Outdated
return True | ||
|
||
if GroupMembershipRoles.ADMIN in authenticated_users_roles: | ||
# Admins can change their own role to anything but admin. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but owner
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, fixed 👍
if not authenticated_users_roles: | ||
return False | ||
|
||
if identity.user.userid == context.user.userid: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are all an user changing it own roles
@@ -133,16 +133,3 @@ def update(context: GroupContext, request): | |||
group = group_update_service.update(context.group, **appstruct) | |||
|
|||
return GroupJSONPresenter(group, request).asdict(expand=["organization", "scopes"]) | |||
|
|||
|
|||
# @TODO This is a duplication of code in h.views.api — move to a util module |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
6 years in the making.
Add an API for editing a group member's role:
Testing:
httpx http://localhost:5000/api/groups/{pubid}/members/acct:devdata_user@localhost --method PATCH --headers Authorization 'Bearer {apitoken}' --json '{"roles": ["admin"]}'
httpx http://localhost:5000/api/groups/{pubid}/members/me --method PATCH --headers Authorization 'Bearer {apitoken}' --json '{"roles": ["admin"]}'
httpx http://localhost:5000/api/groups/{pubid}/members/me --method PATCH --headers Authorization 'Bearer {apitoken}' --json '{"roles": ["INVALID"]}'